package com.chen.base.module.redis.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.chen.base.module.redis.config.RedisLuaLock;
import com.chen.base.module.redis.inter.LockFunctional;
import com.chen.base.module.redis.service.DistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author: Long
 * @date: 2023/5/3 18:27
 **/
@Slf4j
@Service
@SuppressWarnings("AlibabaAvoidManuallyCreateThread")
public class RedisLuaDistributedLock implements DistributedLock<RedisLuaLock> {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /** 成功标识 */
    private static final Long SUCCESS = 1L;

    /** 删除锁的lua脚本对象 */
    private static final DefaultRedisScript<Long> DEL_SCRIPT = new DefaultRedisScript<>(
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "  return redis.call('del', KEYS[1]) " +
                    "else " +
                    "  return 0 " +
                    "end", Long.class
    );

    /**
     * 自旋等待获得锁
     * @param key 锁的key
     * @param expire 锁的失效时间，单位：毫秒
     * @param waitTime 自旋等待时间，单位：毫秒
     * @return 不为NULL：加锁成功
     */
    public LockFunctional tryLock(String key, long expire, long waitTime){
        long endTimestamp = System.currentTimeMillis() + waitTime;

        LockFunctional lockFunctional = null;
        // 自旋条件：没获得锁，或者当前等待时间 小于 自旋等待时间
        while (lockFunctional == null){
            lockFunctional = lock(key, expire);
            if (System.currentTimeMillis() > endTimestamp){
                break;
            }
        }

        return lockFunctional;
    }

    @Override
    public LockFunctional lock(String key, long expire) {
        // 获取随机字符串
        String uuid = UUID.randomUUID().toString();
        // 使用(set key value px 毫秒 nx)命令加锁
        Boolean store = redisTemplate.opsForValue().setIfAbsent(key, uuid, expire, TimeUnit.MILLISECONDS);
        if (BooleanUtil.isTrue(store)){
            // 守护线程：锁续期
            Thread daemonThread = lockRenewal(key, expire);
            // 构建锁对象
            RedisLuaLock lock = new RedisLuaLock(key, uuid, daemonThread);
            return () -> unLock(lock);
        }

        return null;
    }

    @Override
    public Boolean unLock(RedisLuaLock lock) {
        // todo 强制停止续期的守护线程
        lock.getDaemonThread().stop();
        // todo 返回 1 表示删除成功，反之返回 0
        Long delSuccess = redisTemplate.execute(DEL_SCRIPT, Collections.singletonList(lock.getKey()), lock.getValue());
        return SUCCESS.equals(delSuccess);
    }

    /**
     * 守护线程：锁续期
     * @param key 锁的唯一标识
     * @param expire 锁失效时间，单位：毫秒
     * @return 守护线程对象
     */
    private Thread lockRenewal(String key, long expire) {
        Thread parentThread = Thread.currentThread();
        Thread daemonThread = new Thread(() -> {
            // 锁过期时间，毫秒 转 秒
            long expireTime = expire / 1000;
            // 锁续期频率 = 过期时间 / 2
            long frequency = expire / 2;
            // 锁续期是否成功
            boolean exSuccess = true;
            // keys
            List<String> keys = Collections.singletonList(key);

            // todo 第一个参数：lua脚本；第二个参数：KEYS[1]；第三个参数：ARGV[1]。
            String lua = "if redis.call('exists', KEYS[1]) == 1 then " +
                         "  redis.call('expire', KEYS[1], {}) " +
                         "  return 1 " +
                         "else " +
                         "  return 0 " +
                         "end";
            lua = StrUtil.format(lua, expireTime);
            DefaultRedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);

            // 只有 (主线程存活) 并且 (锁存在) 的情况下，才续期
            while (parentThread.isAlive() && exSuccess){
                try {
                    // todo 子线程频繁的阻塞和唤醒，期以后能找到解决方式
                    Thread.sleep(frequency);
                    // todo 返回 1 表示成功，反之返回 0
                    exSuccess = SUCCESS.equals(redisTemplate.execute(script, keys));
                }catch (Exception ignored){}
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        return daemonThread;
    }

}
